﻿using System;
using System.Diagnostics;
using System.Globalization;
using TomanuExtensions;
using YAXLib;

namespace RaytracerLib.MathLib
{
    /// <summary>
    /// General purpose.
    /// </summary>
    [DebuggerStepThrough]
    [DebuggerDisplay("X: {X}; Y: {Y}; Z: {Z}")]
    public struct Vector3
    {
        #region Consts
        public static readonly Vector3 XAXIS = new Vector3(1, 0, 0);
        public static readonly Vector3 YAXIS = new Vector3(0, 1, 0);
        public static readonly Vector3 ZAXIS = new Vector3(0, 0, 1);
        public static readonly Vector3 ZERO = new Vector3(0, 0, 0);
        public static readonly Vector3 ONE = new Vector3(1, 1, 1);
        public static readonly Vector3 HALF = new Vector3(0.5, 0.5, 0.5);

        public static readonly Vector3 POSITIVE_INFINITY =
            new Vector3(Double.PositiveInfinity, Double.PositiveInfinity,
                Double.PositiveInfinity);

        public static readonly Vector3 NEGATIVE_INFINITY =
            new Vector3(Double.NegativeInfinity, Double.NegativeInfinity,
                Double.NegativeInfinity);

        public static readonly Vector3 MINIMUM =
            new Vector3(Constants.DOUBLE_MIN, Constants.DOUBLE_MIN, Constants.DOUBLE_MIN);

        public static readonly Vector3 MAXIMUM =
            new Vector3(Constants.DOUBLE_MAX, Constants.DOUBLE_MAX, Constants.DOUBLE_MAX);

        public static readonly Vector3 UNDEFINED =
            new Vector3(Double.NegativeInfinity, Double.NegativeInfinity,
                Double.NegativeInfinity);
        #endregion

        [YAXAttributeForClass]
        public readonly double X;

        [YAXAttributeForClass]
        public readonly double Y;

        [YAXAttributeForClass]
        public readonly double Z;

        public Vector3(Vector3 a_v)
        {
            X = a_v.X;
            Y = a_v.Y;
            Z = a_v.Z;
        }

        public Vector3(double a_x, double a_y, double a_z)
        {
            X = a_x;
            Y = a_y;
            Z = a_z;
        }

        public double Length
        {
            get
            {
                return Math.Sqrt(SqrLen);
            }
        }

        public double SqrLen
        {
            get
            {
                return X * X + Y * Y + Z * Z;
            }
        }

        public Vector3 Normalized
        {
            get
            {
                double length = Length;

                if (length == 0)
                    return this;

                return new Vector3(X / length, Y / length, Z / length);
            }
        }

        public static Vector3 CrossProduct(Vector3 a_a, Vector3 a_b)
        {
            return new Vector3(
                a_a.Y * a_b.Z - a_a.Z * a_b.Y,
                a_a.Z * a_b.X - a_a.X * a_b.Z,
                a_a.X * a_b.Y - a_a.Y * a_b.X);
        }

        public override int GetHashCode()
        {
            return X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();
        }

        public override string ToString()
        {
            return String.Format("{0}, {1}, {2} ",
                X.ToString(CultureInfo.InvariantCulture),
                Y.ToString(CultureInfo.InvariantCulture),
                Z.ToString(CultureInfo.InvariantCulture));
        }

        public static bool operator ==(Vector3 a_a, Vector3 a_b)
        {
            return (a_a.X == a_b.X) && (a_a.Y == a_b.Y) && (a_a.Z == a_b.Z);
        }

        public static bool operator !=(Vector3 a_a, Vector3 a_b)
        {
            return !(a_a == a_b);
        }

        public override bool Equals(object a_obj)
        {
            if (a_obj == null)
                return false;

            if (!typeof(Vector3).Equals(a_obj.GetType()))
                return false;
            Vector3 vector3d = (Vector3)a_obj;

            return (X == vector3d.X) && (Y == vector3d.Y) && (Z == vector3d.Z);
        }

        public bool Equals(Vector3 a_vector3d)
        {
            return (X == a_vector3d.X) && (Y == a_vector3d.Y) && (Z == a_vector3d.Z);
        }

        public static Vector3 operator +(Vector3 a_a, Vector3 a_b)
        {
            return new Vector3(a_a.X + a_b.X, a_a.Y + a_b.Y, a_a.Z + a_b.Z);
        }

        public static Vector3 operator -(Vector3 a_a, Vector3 a_b)
        {
            return new Vector3(a_a.X - a_b.X, a_a.Y - a_b.Y, a_a.Z - a_b.Z);
        }

        public static Vector3 operator *(Vector3 a_a, double a_b)
        {
            return new Vector3(a_a.X * a_b, a_a.Y * a_b, a_a.Z * a_b);
        }

        public static Vector3 operator /(Vector3 a_a, Vector3 a_b)
        {
            return new Vector3(a_a.X / a_b.X, a_a.Y / a_b.Y, a_a.Z / a_b.Z);
        }

        public static Vector3 operator /(Vector3 a_a, double a_b)
        {
            return new Vector3(a_a.X / a_b, a_a.Y / a_b, a_a.Z / a_b);
        }

        public static Vector3 operator *(double a_a, Vector3 a_b)
        {
            return new Vector3(a_b.X * a_a, a_b.Y * a_a, a_b.Z * a_a);
        }

        public static double operator *(Vector3 a_a, Vector3 a_b)
        {
            return a_a.X * a_b.X + a_a.Y * a_b.Y + a_a.Z * a_b.Z;
        }

        public static Vector3 operator -(Vector3 a_a)
        {
            return new Vector3(-a_a.X, -a_a.Y, -a_a.Z);
        }

        public static bool operator <(Vector3 a_a, Vector3 a_b)
        {
            return (a_a.X < a_b.X) && (a_a.Y < a_b.Y) && (a_a.Z < a_b.Z);
        }

        public static bool operator <=(Vector3 a_a, Vector3 a_b)
        {
            return (a_a.X <= a_b.X) && (a_a.Y <= a_b.Y) && (a_a.Z <= a_b.Z);
        }

        public static bool operator >(Vector3 a_a, Vector3 a_b)
        {
            return (a_a.X > a_b.X) && (a_a.Y > a_b.Y) && (a_a.Z > a_b.Z);
        }

        public static bool operator >=(Vector3 a_a, Vector3 a_b)
        {
            return (a_a.X >= a_b.X) && (a_a.Y >= a_b.Y) && (a_a.Z >= a_b.Z);
        }

        public Vector3 Scale(Vector3 a_value)
        {
            return new Vector3(X * a_value.X, Y * a_value.Y, Z * a_value.Z);
        }

        public static Vector3 Minimize(Vector3 a_v1, Vector3 a_v2)
        {
            if (a_v1.X > a_v2.X)
            {
                if (a_v1.Y > a_v2.Y)
                {
                    if (a_v1.Z > a_v2.Z)
                        return new Vector3(a_v2.X, a_v2.Y, a_v2.Z);
                    else
                        return new Vector3(a_v2.X, a_v2.Y, a_v1.Z);
                }
                else
                {
                    if (a_v1.Z > a_v2.Z)
                        return new Vector3(a_v2.X, a_v1.Y, a_v2.Z);
                    else
                        return new Vector3(a_v2.X, a_v1.Y, a_v1.Z);
                }
            }
            else
            {
                if (a_v1.Y > a_v2.Y)
                {
                    if (a_v1.Z > a_v2.Z)
                        return new Vector3(a_v1.X, a_v2.Y, a_v2.Z);
                    else
                        return new Vector3(a_v1.X, a_v2.Y, a_v1.Z);
                }
                else
                {
                    if (a_v1.Z > a_v2.Z)
                        return new Vector3(a_v1.X, a_v1.Y, a_v2.Z);
                    else
                        return new Vector3(a_v1.X, a_v1.Y, a_v1.Z);
                }
            }
        }

        public static Vector3 Maximize(Vector3 a_v1, Vector3 a_v2)
        {
            if (a_v1.X < a_v2.X)
            {
                if (a_v1.Y < a_v2.Y)
                {
                    if (a_v1.Z < a_v2.Z)
                        return new Vector3(a_v2.X, a_v2.Y, a_v2.Z);
                    else
                        return new Vector3(a_v2.X, a_v2.Y, a_v1.Z);
                }
                else
                {
                    if (a_v1.Z < a_v2.Z)
                        return new Vector3(a_v2.X, a_v1.Y, a_v2.Z);
                    else
                        return new Vector3(a_v2.X, a_v1.Y, a_v1.Z);
                }
            }
            else
            {
                if (a_v1.Y < a_v2.Y)
                {
                    if (a_v1.Z < a_v2.Z)
                        return new Vector3(a_v1.X, a_v2.Y, a_v2.Z);
                    else
                        return new Vector3(a_v1.X, a_v2.Y, a_v1.Z);
                }
                else
                {
                    if (a_v1.Z < a_v2.Z)
                        return new Vector3(a_v1.X, a_v1.Y, a_v2.Z);
                    else
                        return new Vector3(a_v1.X, a_v1.Y, a_v1.Z);
                }
            }
        }

        public static double Angle(Vector3 a_v1, Vector3 a_v2)
        {
            double a = (a_v1 * a_v2) / (a_v1.Length * a_v2.Length);

            Debug.Assert(a.IsAlmostRelativeGreaterOrEqualThen(-1));
            Debug.Assert(a.IsAlmostRelativeLessOrEqualThen(1));

            a = DoubleExtensions.Limit(a, -1, 1);

            return Math.Acos(a);
        }

        public bool IsAlmostRelativeEquals(Vector3 a_v, double a_precision = Constants.DOUBLE_PRECISION)
        {
            return X.IsAlmostRelativeEquals(a_v.X, a_precision) && Y.IsAlmostRelativeEquals(a_v.Y, a_precision) &&
                Z.IsAlmostRelativeEquals(a_v.Z, a_precision);
        }

        public bool IsNormalized
        {
            get
            {
                return SqrLen.IsAlmostRelativeEquals(1);
            }
        }

        public static Vector3 Min(Vector3 a_v1, Vector3 a_v2)
        {
            return new Vector3(
                Math.Min(a_v1.X, a_v2.X),
                Math.Min(a_v1.Y, a_v2.Y),
                Math.Min(a_v1.Z, a_v2.Z));
        }

        public static Vector3 Max(Vector3 a_v1, Vector3 a_v2)
        {
            return new Vector3(
                Math.Max(a_v1.X, a_v2.X),
                Math.Max(a_v1.Y, a_v2.Y),
                Math.Max(a_v1.Z, a_v2.Z));
        }

        public Vector3 Abs()
        {
            return new Vector3(Math.Abs(X), Math.Abs(Y), Math.Abs(Z));
        }

        public double Max()
        {
            return DoubleExtensions.Max(X, Y, Z);
        }

        public double Min()
        {
            return DoubleExtensions.Min(X, Y, Z);
        }

        public bool IsFinite
        {
            get
            {
                return X.IsNumber() && Y.IsNumber() && Z.IsNumber();
            }
        }

        public bool IsAlmostLessThen(Vector3 a_v, double a_precision = Constants.DOUBLE_PRECISION)
        {
            return X.IsAlmostLessThen(a_v.X) && Y.IsAlmostLessThen(a_v.Y) && Z.IsAlmostLessThen(a_v.Z);
        }

        public bool IsAlmostGreaterThen(Vector3 a_v, double a_precision = Constants.DOUBLE_PRECISION)
        {
            return X.IsAlmostGreaterThen(a_v.X) && Y.IsAlmostGreaterThen(a_v.Y) && Z.IsAlmostGreaterThen(a_v.Z);
        }
    }
}